import React, { useEffect, useRef, useMemo, useCallback, useReducer } from 'react'
import axios, { AxiosRequestConfig, CancelTokenSource, AxiosError, AxiosResponse } from 'axios'
import fetchAxios from './axios'
import { useDeepCompare } from '../../hooks/useDeepCompare'
import { useCreation } from '../../hooks/useCreation'

export * from './axios'
export default fetchAxios

export type FetchRes<T> =
	| {
			success: 1
			data?: T
			list?: T[]
			total?: number
			description: string
			error_code: string
	  }
	| {
			success: 0
			description: string
			error_code: string
	  }

export interface IState<IData = any, IError = any> {
	loading: boolean
	error?: null | AxiosError<IError>
	data?: IData
	response?: AxiosResponse<IData>
}

export type IOption = {
	/** 是否手动请求 */
	manual?: boolean
	/** 是否监听config */
	listenConfig?: boolean
	/** 启用共享缓存，key值 */
	cacheKey?: string
	/** 启用共享缓存的缓存保存时间，默认 10ms */
	cacheTime?: number
}

type Option = Required<Omit<IOption, 'cacheKey'>> & Pick<IOption, 'cacheKey'>

enum actions {
	REQUEST_START = 'REQUEST_START',
	REQUEST_END = 'REQUEST_END',
}

interface IAction {
	type: actions
	payload?: any
	error?: boolean
}

const DEFAULT_OPTIONS: Option = {
	manual: false,
	listenConfig: true,
	cacheTime: 10,
}

interface CacheData {
	data?: AxiosResponse<any, any>
	type: actions
	time: number
}

const cache = new Map<string, CacheData>()

function setCache(cacheKey: string, type: actions, response?: AxiosResponse<any, any>) {
	cache.set(cacheKey, {
		data: response,
		type,
		time: Date.now(),
	})
}

function getCache(cacheKey?: string) {
	return cacheKey ? cache.get(cacheKey) : undefined
}

function deleteCache(cacheKey?: string) {
	cacheKey && cache.delete(cacheKey)
}

const resolveCacheMap = new Map<
	string,
	((value: AxiosResponse<any, any> | PromiseLike<AxiosResponse<any, any>>) => void)[]
>()

function setResolveCacheMap(
	cacheKey: string,
	resolve: (value: AxiosResponse<any, any> | PromiseLike<AxiosResponse<any, any>>) => void
) {
	const cacheData = resolveCacheMap.get(cacheKey) || []
	cacheData.push(resolve)
	resolveCacheMap.set(cacheKey, cacheData)
}

function getResolveCacheMap(cacheKey: string) {
	return resolveCacheMap.get(cacheKey)
}

function deleteResolveCacheMap(cacheKey: string) {
	return resolveCacheMap.delete(cacheKey)
}

let listeners: { [key: string]: React.Dispatch<IAction>[] } = {}

function configToObject(config?: string | AxiosRequestConfig): AxiosRequestConfig {
	if (typeof config === 'string') {
		return {
			url: config,
			method: 'get',
		}
	}

	return Object.assign({}, config)
}

function createInitialState<IData, IError>(options: Option): IState<IData, IError> {
	const cacheData = getCache(options.cacheKey)
	if (cacheData) {
		return {
			loading: !options.manual && cacheData.type === actions.REQUEST_START,
			error: null,
			...(cacheData.type === actions.REQUEST_END
				? { data: cacheData.data?.data, response: cacheData.data }
				: null),
		}
	} else {
		return {
			loading: !options.manual,
			error: null,
		}
	}
}

function reducer<IData, IError>(state: IState<IData, IError>, action: IAction): IState<IData, IError> {
	switch (action.type) {
		case actions.REQUEST_START:
			return {
				...state,
				loading: true,
				error: null,
			}
		case actions.REQUEST_END:
			return {
				...state,
				loading: false,
				...(action.error ? {} : { data: action.payload.data }),
				[action.error ? 'error' : 'response']: action.payload,
			}
	}
}

async function actualRequest<IData>(
	config: AxiosRequestConfig,
	dispatch: React.Dispatch<IAction>,
	options: Option
): Promise<AxiosResponse<IData>> {
	try {
		dispatch({ type: actions.REQUEST_START })

		if (options.cacheKey) {
			setCache(options.cacheKey, actions.REQUEST_START)
		}
		const response = await fetchAxios(config)

		if (options.cacheKey) {
			setCache(options.cacheKey, actions.REQUEST_END, response)
			//触发请求未返回时存储的缓存请求
			const resolveCacheData = getResolveCacheMap(options.cacheKey)
			if (resolveCacheData) {
				resolveCacheData.forEach((resolve) => resolve(response))
				deleteResolveCacheMap(options.cacheKey)
			}

			//触发所有的dispatch更新
			listeners[options.cacheKey]?.forEach((listener) =>
				listener({ type: actions.REQUEST_END, payload: response })
			)
		} else {
			dispatch({ type: actions.REQUEST_END, payload: response })
		}

		return response
	} catch (err) {
		if (!axios.isCancel(err)) {
			dispatch({ type: actions.REQUEST_END, payload: err, error: true })
		}

		throw err
	}
}

async function request<IData>(
	config: AxiosRequestConfig,
	dispatch: React.Dispatch<IAction>,
	options: Option,
	forceUpdate: boolean = false
): Promise<AxiosResponse<IData>> {
	if (options.cacheKey && !forceUpdate) {
		const cacheData = getCache(options.cacheKey)
		if (cacheData) {
			//命中缓存有三种情况，第一种是缓存时间超过了，第二种是缓存时间没有超过，但是缓存请求已结束，第三种缓存的请求还未结束。
			if (Date.now() - cacheData.time > options.cacheTime) {
				deleteCache(options.cacheKey)
				return actualRequest(config, dispatch, options)
			} else {
				if (cacheData.type === actions.REQUEST_END) {
					return Promise.resolve(cacheData.data!)
				} else {
					return new Promise((resolve) => {
						setResolveCacheMap(options.cacheKey!, resolve)
					})
				}
			}
		} else {
			return actualRequest(config, dispatch, options)
		}
	} else {
		deleteCache(options.cacheKey)
		return actualRequest(config, dispatch, options)
	}
}

export const useFetch = <IData = any, IError = any>(_config: string | AxiosRequestConfig, _options?: IOption) => {
	// eslint-disable-next-line react-hooks/exhaustive-deps
	const options = useMemo(() => ({ ...DEFAULT_OPTIONS, ..._options }), useDeepCompare(_options))
	// eslint-disable-next-line react-hooks/exhaustive-deps
	const config = useMemo(() => configToObject(_config), useDeepCompare(options.listenConfig ? _config : ''))

	const cancelTokensRef = useRef<CancelTokenSource[]>([])

	const [state, dispatch] = useReducer<(state: IState<IData, IError>, action: IAction) => IState<IData, IError>>(
		reducer,
		createInitialState<IData, IError>(options)
	)

	useCreation(() => {
		if (options.cacheKey) {
			listeners[options.cacheKey]
				? listeners[options.cacheKey].push(dispatch)
				: (listeners[options.cacheKey] = [dispatch])
		}
	}, [])

	const intervalId = useRef<number>()

	const cancelFetch = useCallback(() => {
		cancelTokensRef.current.forEach((x) => x.cancel())
		cancelTokensRef.current = []
		clearTimeout(intervalId.current)
		intervalId.current = undefined
	}, [])

	const withCancelToken = useCallback(
		(config, cancelBefore = true) => {
			cancelBefore && cancelFetch()

			const source = axios.CancelToken.source()
			cancelTokensRef.current.push(source)

			config.cancelToken = source.token

			return config
		},
		[cancelFetch]
	)

	useEffect(() => {
		if (!options.manual) {
			request<IData>(withCancelToken(config), dispatch, options).catch(() => {})
		}
		return cancelFetch
	}, [config, options, withCancelToken, cancelFetch])

	const reFetch = useCallback(
		(configOverride?: string | AxiosRequestConfig, cancelBefore?: boolean) => {
			configOverride = configToObject(configOverride)

			return request<IData>(
				withCancelToken({ ...config, ...configOverride }, cancelBefore),
				dispatch,
				options,
				true
			)
		},
		[config, options, withCancelToken]
	)

	return [state, reFetch, cancelFetch] as const
}
